-- vim: sw=4 sts=4 et:
--[[
-- GPLv3
--]]

-- 0. connect to an irc server
-- 1. get full list of channels, save it somewhere (wait)
-- 2. join a channel
-- 3. save a list of names on the channel (wait)
-- 4. leave the channel (wait)
-- 5. repeat from 2. until theres no moer channels

local socket = require 'socket'
local cjson = require 'cjson'
require 'debugger'
irc = {}

nick = 'mlilenium_'
user = 'mlilenium'


function errprintf (fmt, ...) return io.stderr:write(string.format(fmt, ...)) end
function printf    (fmt, ...) return io.stdout:write(string.format(fmt, ...)) end
function sleep (s) socket.select(nil, nil, s) end


function sendall (s, data)
    local a, b = 0, #data
    repeat
        a = a + 1
        a = assert(s:send(data, a, b))
    until a == #data
    errprintf("sent { %q }\n", data)
    return true
end


function receivelines (sock, msgs, delay)
    if msgs == nil then msgs = {} end
    if delay == nil then delay = 0.1 end
    local msg, errstr, partmsg
    local recvt, sendt
    repeat
        recvt, sendt, errstr = socket.select({sock}, nil, delay)
        if type(recvt) == 'table' and recvt[sock] ~= nil then
            msg, errstr, partmsg = sock:receive('*l')  -- XXX
            if msg ~= nil then
                table.insert(msgs, msg)
            else
                errprintf("receivelines: error: %s\n", errstr)
                break
            end
        end
    until type(recvt) == 'table' and recvt[sock] == nil
    return msgs
end


function irc.connect (server, port, s)
    local cn = assert(socket.tcp())
    errprintf("connecting to %s:%d\n", server, port)
    assert(cn:connect(server, port))
    cn:setoption('tcp-nodelay', true)
    local connmsg = 'NICK '..nick..'\r\nUSER '..user..' 0 * :'..user..'\r\n'
    sleep(2)
    sendall(cn, connmsg)

    local rt, st, errstr, ret, msg
    repeat
        rt, st, errstr = socket.select({cn}, nil, 120)
        receivelines(cn, s.msg_q)
        while #s.msg_q > 0 and ret ~= 'end of MOTD' do
            msg = table.remove(s.msg_q, 1)
            ret = irc.handle_msg(cn, msg, s)
        end
    until ret == 'end of MOTD'

    return cn
end
function irc.ping_server (irccn, server, s)
    sendall(irccn, 'PING '..server..'\r\n')
end
function irc.list (irccn, s)
    local rt, st, err, ret
    sendall(irccn, 'LIST\r\n')
    repeat
        rt, st, err = socket.select({irccn}, nil, 60)
        receivelines(irccn, s.msg_q)
        while #s.msg_q > 0 and ret ~= 'end of LIST' do
            ret = irc.handle_msg(irccn, s.msg_q[1], s)
            table.remove(s.msg_q, 1)
        end
    until ret == 'end of LIST'
end
function irc.join (irccn, s)
    sendall(irccn, 'JOIN ' .. s.channels_q[1].channel .. '\r\n')
end
function irc.part (irccn, s)
    sendall(irccn, 'PART ' .. s.channels_q[1].channel .. '\r\n')
end
function irc.quit (irccn, s)
    local msg, ret
    sendall(irccn, 'QUIT :Gone to have lunch\r\n')
    sleep(2)
    receivelines(irccn, s.msg_q)
    while #s.msg_q > 0 do
        ret = irc.handle_msg(irccn, s.msg_q[1], s)
        table.remove(s.msg_q, 1)
    end
    irccn:close()
    return true
end


function irc.handle_msg (irccn, msg, s)
    assert(type(msg) == 'string')
    errprintf("handling msg: { %q }\n", msg)

    if msg:match('^PING') or msg:match('^:%S+ PING') then
        if msg:match('PING :%S+') then
            local value = msg:match('PING (:.*)$')
            sendall(irccn, 'PONG '..value..'\r\n')
        else
            sendall(irccn, 'PONG\r\n')
        end
    elseif msg:match('^:%S+ 353 ') or msg:match('^353 ') then
        -- hanlde /NAMES reply
        local chan, names = msg:match('353 %S+ [=@*] (%S+) :(.*)$')
        if type(s.g[chan]) ~= 'table' then
            s.g[chan] = {}
        end
        for name in names:gmatch('%S+') do
            if name:sub(1, 1):match('^[%~%&%@%%%+]$') then
                name = name:sub(2)
            end
            table.insert(s.g[chan], name)
        end
    elseif msg:match('^:%S+ 366 ') or msg:match('^366 ') then
        return 'end of NAMES'
    elseif msg:match('^:%S+ 322 ') or msg:match('^322 ') then
        -- handle /LIST reply
        local chan, usrnum, topic = msg:match('322 %S+ (%S+) (%d+) :(.*)$')
        usrnum = tonumber(usrnum) or 0
        table.insert(s.channels_q, { channel = chan, users = usrnum })
        errprintf("added channel %s with %d users\n", chan, usrnum)
    elseif msg:match('^:%S+ 323 ') or msg:match('^323 ') then
        return 'end of LIST'
    elseif msg:match('^:%S+ 470 ') or msg:match('^470 ') then
        local oldchan, newchan = msg:match('470 %S+ (%S+) (%S+)')
        if s.channels_q[1].channel == oldchan then
            s.channels_q[1].channel = newchan
        end
        --return 'retry'
    elseif msg:match('^:%S+ 376 ') or msg:match('^376 ') or
            msg:match('^:%S+ 422 ') or msg:match('^422 ') then
        return 'end of MOTD'
    elseif msg:match('^:%S+ 439 ') or msg:match('^439 ') then
        local sleepsecs = msg:match('(%d+) second') or '60'
        sleepsecs = tonumber(sleepsecs)
        sleep(sleepsecs + 2)
        return 'retry'
    elseif msg:match('^:%S+ 33[12] ') or msg:match('^33[12] ') then
        local chan = msg:match('^:%S+ 33[12] %S+ (%S+) ') or msg:match('^33[12] %S+ (%S+)')
        return 'joined '..chan
    elseif msg:match('^:%S+ 442 ') or msg:match('^442 ') then
        local chan = msg:match('^:%S+ 442 %S+ (%S+)') or msg:match('^442 %S+ (%S+)')
        return 'parted ' ..chan
    elseif msg:match('^:%S+ 47[134567] ') or msg:match('^47[134567] ') then
        local chan = msg:match('47%d %S+ (%S+)')
        return 'joined '..chan
    end
end


function save_state (statefilename, state)
    local statefile = assert(io.open(statefilename, 'w'))
    statefile:write(cjson.encode(state))
    statefile:write('\n')
    statefile:flush()
    statefile:close()
end


function keys_num (t)
    local n = 0
    for k in pairs(t) do
        n = n + 1
    end
    return n
end


function main (args)
    local statefilename = args[2] or 'irc_crawl.json'
    local server, port = string.match(args[1], '^(.+):(%d+)$')
    port = tonumber(port)
    local rt, st, err
    local ret

    local statefile = io.open(statefilename, 'r')
    if statefile == nil then
        statefile = assert(io.open(statefilename, 'w+'))
    end
    local statestr = statefile:read('*a')
    statefile:close()
    statefile = nil
    if statestr and statestr ~= '' then
        s = cjson.decode(statestr)
    else
        s = { channels_q = {}, g = {}, msg_q = {} }
    end

    local cn = irc.connect(server, port, s)
    sleep(3)
    if #s.channels_q == 0 and keys_num(s.g) == 0 then
        irc.list(cn, s)
        table.sort(s.channels_q, function (a, b) return a.users > b.users end)
        save_state(statefilename, s)
        sleep(3)
    end

    while #s.channels_q > 0 do
        errprintf("joining channel %s (%d remaining)\n", s.channels_q[1].channel, #s.channels_q)
        repeat
            irc.join(cn, s)
            repeat
                rt, st, err = socket.select({cn}, nil, 10)
                receivelines(cn, s.msg_q)
                ret = nil
                while #s.msg_q > 0 and ret ~= 'end of NAMES' and ret ~= 'retry' do
                    ret = irc.handle_msg(cn, s.msg_q[1], s)
                    table.remove(s.msg_q, 1)
                end
            until ret == 'end of NAMES' or ret == 'retry' or
                  ret == 'joined '..s.channels_q[1].channel
        until ret == 'end of NAMES' or ret == 'joined '..s.channels_q[1].channel
        sleep(math.random(5, 10))
        errprintf("leaving channel %s (%d remaining)\n", s.channels_q[1].channel, #s.channels_q)
        irc.part(cn, s)
        table.remove(s.channels_q, 1)
        save_state(statefilename, s)
        sleep(math.random(10, 20))
    end
    irc.quit(cn)
    save_state(statefilename, s)
end


math.randomseed(os.time())
main(arg)
